use crate::core::core::Client;
use crate::types::{Auth, DecodeError, Error, StatusMismatchedError};
use digest_auth::{AuthContext, HttpMethod};
use http::Method;
use reqwest::RequestBuilder;
use std::ops::Deref;
use url::Url;

impl Client {
    pub(crate) async fn apply_authentication(
        &self,
        mut builder: RequestBuilder,
        method: &Method,
        url: &Url,
    ) -> Result<RequestBuilder, Error> {
        match &self.auth {
            Auth::Anonymous => {}
            Auth::Basic(username, password) => {
                builder = builder.basic_auth(username, Some(password));
            }
            Auth::Digest(username, password) => {
                self.setup_digest_auth_if_not_initialized(method, url)
                    .await?;
                let mut context = AuthContext::new(username, password, url.path());
                context.method = HttpMethod::from(method.to_string());
                let mut digest_state_lock = self.digest_auth.lock().await;
                match digest_state_lock.as_mut() {
                    None => return Err(Error::MissingAuthContext),
                    Some(state) => {
                        let response = state.respond(&context)?;
                        builder = builder.header("Authorization", response.to_header_string());
                    }
                }
            }
        };
        Ok(builder)
    }

    async fn is_digest_auth_initialized(&self) -> bool {
        self.digest_auth.lock().await.deref().is_some()
    }

    async fn setup_digest_auth_if_not_initialized(
        &self,
        method: &Method,
        url: &Url,
    ) -> Result<(), Error> {
        if !self.is_digest_auth_initialized().await {
            self.probe_server_for_digest_auth(method, url).await?;
        }
        Ok(())
    }

    async fn probe_server_for_digest_auth(&self, method: &Method, url: &Url) -> Result<(), Error> {
        let response = self
            .agent
            .request(method.clone(), url.as_str())
            .send()
            .await?;
        let code = response.status().as_u16();
        if code == 401 {
            let headers = response.headers();
            let www_auth = headers
                .get("www-authenticate")
                .ok_or(Error::Decode(DecodeError::NoAuthHeaderInResponse))?
                .to_str()?;
            self.update_auth_context(www_auth).await?;
            Ok(())
        } else {
            Err(Error::Decode(DecodeError::StatusMismatched(
                StatusMismatchedError {
                    response_code: code,
                    expected_code: 401,
                },
            )))
        }
    }

    async fn update_auth_context(&self, auth_header: &str) -> Result<(), Error> {
        let auth_context = digest_auth::parse(auth_header)?;
        let mut session_auth = self.digest_auth.lock().await;
        *session_auth = Some(auth_context);
        Ok(())
    }
}
